package r25.engine;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.slf4j.Logger;

import r25.board.ControlMove;
import r25.board.IBoard;
import r25.board.RoomPosition;
import r25.character.ICharacter;
import r25.character.IInRoomObject;
import r25.engine.CharacterActionResponse.ERROR_REASON;
import r25.engine.IEvent.EVENT;
import r25.engine.IEvent.EVENT_TYPE;
import r25.engine.IGameService.CHARACTER_ROLE;
import r25.engine.IGameService.GAME_MODE;
import r25.engine.IGameService.GAME_PHASE;
import r25.room.IRoom;
import r25.tools.IdGenerator;
import r25.tools.LoggerHelper;

public class GameInstance implements IGameInstance {
	private final String id;
	private final IBoard board;
	private final GAME_MODE mode;

	private final List<String> players = new ArrayList<String>();
	private final List<ICharacter> characters = new ArrayList<ICharacter>();
	private final List<Integer> characterCountAllowedActions = new ArrayList<Integer>();
	private final List<List<CHARACTER_ACTION>> characterTurnActions = new ArrayList<List<CHARACTER_ACTION>>();
	private final List<List<CHARACTER_ACTION>> characterProgrammedActions = new ArrayList<List<CHARACTER_ACTION>>();
	private final List<ICharacter> winners = new ArrayList<ICharacter>();
	private final List<IEvent> gameEvents = new ArrayList<IEvent>();
	private final List<IEvent> turnEvents = new ArrayList<IEvent>();

	private static final Logger logger = LoggerHelper.getLogger();
	private static final int MAX_CHARACTER_ACTIONS = 2;
	private final IGameService gs;

	// state machine
	private boolean started;
	private boolean over;
	private GAME_PHASE currentGamePhase;
	private int currentTurn;
	private int currentCharacter;
	private int currentCharacterAction;
	private CHARACTER_ACTION_STATUS currentCharacterActionStatus;
	private CHARACTER_ACTION currentCharacterExtraAction;

	public GameInstance(GameService gameService, IBoard board, GAME_MODE mode,
			List<String> players) throws Exception {
		this.id = IdGenerator.createId();
		this.board = board;
		this.mode = mode;
		this.players.addAll(players);
		this.gs = gameService;
	}

	@Override
	public String getId() {
		return id;
	}

	@Override
	public IBoard getBoard() {
		return board;
	}

	@Override
	public GAME_MODE getMode() {
		return mode;
	}

	@Override
	public void start() throws Exception {
		winners.clear();

		// TODO be careful that in TEAM mode the players are in 2 teams,
		// implicitely in alternance - teams are important to determine the
		// winning players

		// validate number of players vs GAME_MODE
		int np = getMaxPlayer();
		if (!gs.computePossibleNumberOfPlayers(getMode()).contains(np)) {
			throw new Exception("Invalid number of players: " + np + " vs "
					+ gs.computePossibleNumberOfPlayers(getMode()));
		}

		// generate nc ICharacter's vs number of players
		int nc = np * gs.computeNumberOfCharactersPerPlayer(mode, np);
		List<ICharacter> chc = gs.getCompositionCharacters();
		List<Integer> pl = gs.computePlayerDistribution(mode, np, nc);
		for (int i = 0; i < nc; i++) {
			ICharacter c = gs.pickRandomCharacter(chc).newInstance();
			c.setGameInstance(this);
			c.setRole(CHARACTER_ROLE.PRISONER);
			c.setPlayer(pl.get(i));
			characters.add(c);
		}

		// attribute ng ICharacter.ROLE vs GAME_MODE
		int ng = gs.computeNumberOfGuards(mode, nc);
		List<ICharacter> lg = new ArrayList<ICharacter>();
		lg.addAll(characters);
		for (int i = 0; i < ng; i++) {
			ICharacter guard = gs.pickRandomCharacter(lg);
			int cpos = 0;
			for (ICharacter c : characters) {
				if (c.equals(guard)) {
					characters.get(cpos).setRole(CHARACTER_ROLE.GUARD);
					break;
				}
				cpos++;
			}
		}

		gameEvents.clear();
		turnEvents.clear();
		board.start(this);

		over = false;
		started = true;
		pushEvent(new Event(EVENT.START, null, null));

		// set characters in initial positions
		IRoom centralRoom = board.getRoomByName("CentralRoom");
		for (ICharacter c : characters) {
			c.moveInRoom(centralRoom);
		}

		currentTurn = 1;
		onStartOfTurn();
	}

	@Override
	public boolean isStarted() {
		return started;
	}

	@Override
	public boolean isGameOver() {
		return over;
	}

	@Override
	public List<ICharacter> getWinners() {
		return winners;
	}

	@Override
	public List<String> getPlayers() {
		return players;
	}

	@Override
	public int getCharacterActionIndex() {
		return currentCharacterAction;
	}

	@Override
	public int getTurnIndex() {
		return currentTurn;
	}

	public GAME_PHASE getCurrentGamePhase() {
		return currentGamePhase;
	}

	@Override
	public int getMaxTurn() {
		return mode.getMaxTurn();
	}

	@Override
	public int getCurrentCharacterMaxAction() {
		return characterCountAllowedActions.get(currentCharacter);
	}

	@Override
	public ICharacter getCurrentCharacter() {
		return characters.get(currentCharacter);
	}

	@Override
	public int getCharacterIndex() {
		return currentCharacter;
	}

	@Override
	public int getMaxPlayer() {
		return players.size();
	}

	@Override
	public int getMaxCharacter() {
		return characters.size();
	}

	@Override
	public List<ICharacter> getCharactersTurnOrder() {
		List<ICharacter> rc = new ArrayList<ICharacter>();
		rc.addAll(characters);
		return rc;
	}

	@Override
	public List<ICharacter> getOtherCharactersInRoomObjects(IRoom room,
			ICharacter currentCharacter) {
		List<ICharacter> rc = new ArrayList<ICharacter>();

		for (ICharacter c : characters) {
			if (room.equals(c.getRoomIn()) && !c.equals(currentCharacter)) {
				rc.add(c);
			}
		}
		return rc;
	}

	@Override
	public List<IInRoomObject> getInRoomObjects(IRoom room,
			boolean withReminderToken) {
		List<IInRoomObject> rc = new ArrayList<IInRoomObject>();

		for (ICharacter c : characters) {
			if (room.equals(c.getRoomIn())) {
				rc.add(c);
			}
			if ((withReminderToken) && (!c.isReminderTokenUsable())
					&& (room.equals(c.getReminderToken().getRoomIn()))) {
				rc.add(c.getReminderToken());
			}
		}
		return rc;
	}

	@Override
	public GameStateTimeStamp getCurrentStateTimeStamp() {
		return new GameStateTimeStamp(getId(), currentGamePhase, currentTurn,
				currentCharacter, currentCharacterAction, this.getProgress());
	}

	protected Float getProgress() {
		Float turnAmount = 1.0f / getMaxTurn();
		Float insideTurnAmount = turnAmount / 2;
		Float rc = new Float(getTurnIndex() - 1) / getMaxTurn();

		// in current turn, add character & action progress
		rc += (insideTurnAmount * (new Float(getCharacterIndex()) / getMaxCharacter()));
		rc += (insideTurnAmount * (new Float(currentCharacterAction) / 2));

		return rc;
	}

	@Override
	public void pushEvent(IEvent event) {
		// add GameStateTimeStamp
		event.setTimeStamp(getCurrentStateTimeStamp());
		logger.debug("{}", event);

		// CHECK events have to be processed later
		if (EVENT_TYPE.CHECK.equals(event.getType().getType())) {
			turnEvents.add(event);
			logger.debug("CHECK Event stored: {}", event);
		}

		// All events are logged
		gameEvents.add(event);
	}

	@Override
	public List<IEvent> getGameLogEvents() {
		return gameEvents;
	}

	@Override
	public List<IEvent> getCheckEvents() {
		return turnEvents;
	}

	@Override
	public boolean endCharacterActionPhase() {
		ICharacter character = getCurrentCharacter();
		if (!CHARACTER_ACTION_STATUS.END.equals(currentCharacterActionStatus)) {
			logger.warn("Current character has to play an action: {}",
					character);
			return false;
		}

		onEndOfCharacterPlay();
		incrementCharacter();

		return true;
	}

	protected void onEndOfCharacterPlay() {
		logger.debug("onEndOfCharacterPlay() #{}", turnEvents.size());

		for (IEvent s : turnEvents) {
			if (s.isValid()) {
				s.getRoom().check(this, false, s);
			}
		}
	}

	protected void onEndOfProgramAction() {
		// TODO increment game phase if complete: characterProgrammedActions
		// contains all characterCountAllowedActions

		// currentGamePhase = GAME_PHASE.PLAY_ACTIONS;
	}

	protected void incrementCharacter() {
		logger.debug("incrementCharacter() from character #{}",
				currentCharacter);

		// increment currentCharacter to new alive character that still has
		// actions to do
		int nc = 1 + currentCharacter;
		while (nc < getMaxCharacter()) {
			if ((!characters.get(nc).isAlive())
					|| characterCountAllowedActions.get(nc) <= characterTurnActions
							.get(nc).size()) {
				logger.debug("Skipped inactive character: {}", nc);
				// TODO may add an Event for skipped character
				nc++;
			} else {
				logger.debug("Selected character: {}", nc);
				break;
			}
		}

		if ((nc == getMaxCharacter()) && (currentCharacter < 0)) {
			logger.debug("Skipped all inactive character #{}", nc);
			// no character can play this turn
			onEndOfTurn();
			return;
		}

		// increment currentCharacterAction if overflow
		if (nc == getMaxCharacter()) {
			onEndOfPlayActionsTurnPhase();
		} else {
			currentCharacter = nc;
		}

		currentCharacterActionStatus = CHARACTER_ACTION_STATUS.ACTION;
	}

	protected void onEndOfPlayActionsTurnPhase() {
		logger.debug("onEndOfPlayActionsTurnPhase()");

		currentCharacterAction++;
		if (currentCharacterAction < MAX_CHARACTER_ACTIONS) {
			currentCharacter = -1;
			incrementCharacter();
		} else {
			onEndOfTurn();
		}
	}

	protected void onGameOver(GAME_WINNER winner) {
		over = true;
		pushEvent(new Event(EVENT.OVER, null, null));

		// basic winner conditions, add more test for COMPETITION mode
		if (GAME_WINNER.GAME.equals(winner)) {
			for (ICharacter c : characters) {
				if (CHARACTER_ROLE.GUARD.equals(c.getRole())) {
					winners.add(c);
					pushEvent(new Event(EVENT.WINNER, null, c));
				}
			}
		} else {
			for (ICharacter c : characters) {
				if (CHARACTER_ROLE.PRISONER.equals(c.getRole())) {
					winners.add(c);
					pushEvent(new Event(EVENT.WINNER, null, c));
				}
			}
		}
	}

	protected void onStartOfTurn() {
		logger.debug("onStartOfTurn()");

		if (isGameOver())
			return;

		characterTurnActions.clear();
		characterProgrammedActions.clear();
		characterCountAllowedActions.clear();
		for (ICharacter c : characters) {
			characterTurnActions.add(new ArrayList<CHARACTER_ACTION>());
			characterProgrammedActions.add(new ArrayList<CHARACTER_ACTION>());
			characterCountAllowedActions.add(c.countCharacterActions());
		}
		logger.debug("onStartOfTurn characterTurnOrder {}", characters);
		logger.debug("onStartOfTurn characterCountAllowedActions {}",
				characterCountAllowedActions);

		currentCharacter = -1;
		incrementCharacter();
		// to be sure if any character may play
		if (isGameOver())
			return;

		currentCharacterAction = 0;
		currentCharacterActionStatus = CHARACTER_ACTION_STATUS.ACTION;

		currentGamePhase = ((getMode().hasToProgramActions()) ? GAME_PHASE.PROGRAM_ACTIONS
				: GAME_PHASE.PLAY_ACTIONS);
	}

	protected void onEndOfTurn() {
		logger.debug("onEndOfTurn() @{}", getCurrentStateTimeStamp());

		// copy or remove delayed events
		Iterator<IEvent> i = turnEvents.iterator();
		while (i.hasNext()) {
			IEvent s = i.next();

			if (s.isValid()) {
				s.getRoom().check(this, true, s);
				// the event may just be processed
				if (!s.isValid()) {
					logger.debug("Removing processed event {}", s);
					i.remove();
				}
			} else {
				i.remove();
			}
		}

		// roll characters list: the previous first player becomes the last one
		List<ICharacter> tmp = new ArrayList<ICharacter>();
		for (int j = 1; j < getMaxCharacter(); j++) {
			tmp.add(characters.get(j));
		}
		tmp.add(characters.get(0));
		characters.clear();
		characters.addAll(tmp);

		// increment currentTurn, reset character & action
		currentTurn++;
		logger.debug("new Character order {} @{}", characters,
				getCurrentStateTimeStamp());

		// check game over conditions
		if (currentTurn > getMaxTurn()) {
			onGameOver(GAME_WINNER.GAME);
		}

		getBoard().onNextTurn();
		onStartOfTurn();
	}

	@Override
	public CharacterActionResponse passCharacterAction() {
		// in SOLO mode: pass if no available action

		// TODO on programmed modes: verify if the current action is void,
		// otherwise force the character to play
		if (availableCharacterActions().size() == 0) {
			currentCharacterActionStatus = CHARACTER_ACTION_STATUS.END;
			return new CharacterActionResponse(true,
					ERROR_REASON.CANCELED_ACTION);
		}

		return new CharacterActionResponse(false, ERROR_REASON.INTERNAL_ERROR);
	}

	@Override
	public CharacterActionResponse playReminderToken(
			RoomPosition targetRoomPosition) {
		ICharacter character = getCurrentCharacter();

		if (character.isReminderTokenUsable()) {
			IRoom room = getBoard().getRoomAt(targetRoomPosition);
			if (room != null) {
				pushEvent(new Event(EVENT.PLAY_REMINDER_TOKEN, room, character));
				try {
					character.getReminderToken().moveInRoom(room);
					return new CharacterActionResponse(true, ERROR_REASON.OK);
				} catch (Exception e) {
					logger.warn("", e);
				}
			}
		}
		return new CharacterActionResponse(false, ERROR_REASON.INVALID_ACTION);
	}

	@Override
	public CharacterActionResponse executeCharacterExtraAction(
			CHARACTER_ACTION action, RoomPosition targetRoomPosition,
			ICharacter targetCharacter, ControlMove controlMove) {
		if (!CHARACTER_ACTION_STATUS.EXTRA_ACTION
				.equals(currentCharacterActionStatus)) {
			logger.warn("Wrong executeCharacterAction vs CHARACTER_ACTION_STATUS: "
					+ currentCharacterActionStatus);
			return new CharacterActionResponse(false,
					ERROR_REASON.INVALID_ACTION);
		}
		if (!action.equals(currentCharacterExtraAction)) {
			logger.warn("Wrong ExtraAction: " + action + " vs "
					+ currentCharacterExtraAction);
			return new CharacterActionResponse(false,
					ERROR_REASON.INVALID_ACTION);
		}

		CharacterActionResponse rc = new CharacterActionResponse(true,
				ERROR_REASON.CANCELED_ACTION);

		logger.debug("Performing validated extra action: {}", action);

		ICharacter character = getCurrentCharacter();
		// implement the 2 extra actions
		switch (action) {
		case CONTROL:
			// check controlMove vs character allowed control
			List<ControlMove> acms = getBoard().getValidControlMoves();
			if (acms.contains(controlMove)) {
				try {
					getBoard().controlRooms(controlMove);
					pushEvent(new Event(EVENT.CHARACTER_CONTROL_ACTION, null,
							character));

					rc = new CharacterActionResponse(true, ERROR_REASON.OK);
					currentCharacterActionStatus = CHARACTER_ACTION_STATUS.END;
				} catch (Exception e) {
					rc = new CharacterActionResponse(false,
							ERROR_REASON.INTERNAL_ERROR);
					rc.setMessage(LoggerHelper.getStackTrace(e));
				}
			}
			break;
		case LOOK:
			// check room vs hidden rooms
			List<RoomPosition> ars = getBoard().getHiddenRoomPositions();
			logger.debug("execute LOOK {} ?contains {}", ars,
					targetRoomPosition);
			if (ars.contains(targetRoomPosition)) {
				IRoom room = getBoard().getRoomAt(targetRoomPosition);
				pushEvent(new Event(EVENT.CHARACTER_LOOK_ACTION, room,
						character));

				rc = new CharacterActionResponse(true, ERROR_REASON.OK);
				rc.setRoom(room);
				currentCharacterActionStatus = CHARACTER_ACTION_STATUS.END;
			}
			break;
		default:
			break;
		}

		// progress in character wf
		currentCharacterActionStatus = CHARACTER_ACTION_STATUS.END;

		return rc;
	}

	@Override
	public CharacterActionResponse executeCharacterAction(
			CHARACTER_ACTION action, RoomPosition targetRoomPosition,
			ICharacter targetCharacter, ControlMove controlMove) {
		if (isGameOver()) {
			logger.warn("Game over");
			return new CharacterActionResponse(false,
					ERROR_REASON.INVALID_ACTION);
		}
		if (!CHARACTER_ACTION_STATUS.ACTION
				.equals(currentCharacterActionStatus)) {
			logger.warn("Wrong executeCharacterAction vs CHARACTER_ACTION_STATUS: "
					+ currentCharacterActionStatus);
			return new CharacterActionResponse(false,
					ERROR_REASON.INVALID_ACTION);
		}

		// check character
		ICharacter character = getCurrentCharacter();
		if (!character.isAlive()) {
			logger.warn("Current character is dead: {}", character);
			return new CharacterActionResponse(false,
					ERROR_REASON.CANCELED_ACTION);
		}
		if (!character.availableCharacterActions().contains(action)) {
			logger.warn("Current character can not perform this action: {} {}",
					action, character);
			return new CharacterActionResponse(false,
					ERROR_REASON.INVALID_ACTION);
		}

		// check if character has a free action
		int maxActionsOfCharacter = characterCountAllowedActions
				.get(getCharacterIndex());
		if (characterTurnActions.get(getCharacterIndex()).size() >= maxActionsOfCharacter) {
			logger.warn("Current character has no action: {}", character);
			return new CharacterActionResponse(false,
					ERROR_REASON.INVALID_ACTION);
		}

		// store validated action
		characterTurnActions.get(getCharacterIndex()).add(action);

		// set in in the case the action does nothing
		CharacterActionResponse rc = new CharacterActionResponse(true,
				ERROR_REASON.CANCELED_ACTION);

		List<RoomPosition> ars;
		IRoom room;
		// check & do action
		logger.debug("Performing validated action: {}", action);
		switch (action) {
		case CONTROL:
			// check controlMove vs character allowed control
			List<ControlMove> acms = getBoard().getValidControlMoves(
					character.getRoomIn().getRoomPosition());
			if (acms.contains(controlMove)) {
				try {
					getBoard().controlRooms(controlMove);
					pushEvent(new Event(EVENT.CHARACTER_CONTROL_ACTION, null,
							character));

					rc = new CharacterActionResponse(true, ERROR_REASON.OK);
					currentCharacterActionStatus = CHARACTER_ACTION_STATUS.END;
				} catch (Exception e) {
					rc = new CharacterActionResponse(false,
							ERROR_REASON.INTERNAL_ERROR);
					rc.setMessage(LoggerHelper.getStackTrace(e));
				}
			}
			break;
		case LOOK:
			// check room vs adjacent rooms
			ars = getBoard().getAdjacentRoomPositions(character.getRoomIn(),
					false);
			logger.debug("execute LOOK {} ?contains {}", ars,
					targetRoomPosition);
			if (ars.contains(targetRoomPosition)) {
				room = getBoard().getRoomAt(targetRoomPosition);
				pushEvent(new Event(EVENT.CHARACTER_LOOK_ACTION, room,
						character));

				rc = new CharacterActionResponse(true, ERROR_REASON.OK);
				rc.setRoom(room);
				currentCharacterActionStatus = CHARACTER_ACTION_STATUS.END;
			}
			break;
		case MOVE:
			// check room vs adjacent rooms
			ars = getBoard().getAdjacentRoomPositions(character.getRoomIn(),
					true);
			room = getBoard().getRoomAt(targetRoomPosition);
			logger.debug("execute MOVE {} ?contains {}", ars, room);
			if (ars.contains(targetRoomPosition) && room.isAccessible()) {
				try {
					// MOVE may give an immediate, adjacent free, extra action
					CHARACTER_ACTION extraAction = character.moveInRoom(room);
					pushEvent(new Event(EVENT.CHARACTER_MOVE_ACTION, room,
							character));

					rc = new CharacterActionResponse(true, ERROR_REASON.OK);
					// do not use directly room in case of a cascading move eg
					// VortexRoom
					rc.setRoom(character.getRoomIn());
					rc.setExtraAction(extraAction);

					if (extraAction != null) {
						currentCharacterActionStatus = CHARACTER_ACTION_STATUS.EXTRA_ACTION;
						currentCharacterExtraAction = extraAction;
					} else {
						currentCharacterActionStatus = CHARACTER_ACTION_STATUS.END;
					}
				} catch (Exception e) {
					rc = new CharacterActionResponse(false,
							ERROR_REASON.INTERNAL_ERROR);
					rc.setMessage(LoggerHelper.getStackTrace(e));
				}
			}
			break;
		case PUSH:
			ars = getBoard().getAdjacentRoomPositions(character.getRoomIn(),
					true);
			room = getBoard().getRoomAt(targetRoomPosition);
			// check room vs adjacent rooms
			// check targetCharacter vs in same room as character
			if (ars.contains(targetRoomPosition)
					&& room.isAccessible()
					&& targetCharacter.isAlive()
					&& targetCharacter.getRoomIn()
							.equals(character.getRoomIn())) {
				try {
					targetCharacter.moveInRoom(room);

					pushEvent(new Event(EVENT.CHARACTER_PUSH_ACTION,
							character.getRoomIn(), character));
					pushEvent(new Event(EVENT.IS_PUSHED, room, targetCharacter));

					rc = new CharacterActionResponse(true, ERROR_REASON.OK);
					rc.setRoom(room);
					currentCharacterActionStatus = CHARACTER_ACTION_STATUS.END;
				} catch (Exception e) {
					rc = new CharacterActionResponse(false,
							ERROR_REASON.INTERNAL_ERROR);
				}
			}
			break;
		}

		return rc;
	}

	@Override
	public void programCharacterAction(int characterIndex,
			CHARACTER_ACTION action) throws Exception {
		if (isGameOver())
			return;

		if (GAME_PHASE.PROGRAM_ACTIONS.equals(getCurrentGamePhase())) {
			// TODO validate programmed action: available + unique

			// TODO store programmed action

			onEndOfProgramAction();
		} else {
			throw new Exception(
					"Program Character action is not allowed in phase: "
							+ getCurrentGamePhase());
		}
	}

	@Override
	public List<CHARACTER_ACTION> availableCharacterActions() {
		IRoom room = getCurrentCharacter().getRoomIn();
		List<CHARACTER_ACTION> rc = room.availableCharacterActions();
		for (CHARACTER_ACTION pa : characterTurnActions
				.get(getCharacterIndex())) {
			rc.remove(pa);
		}
		return rc;
	}

	@Override
	public CHARACTER_ACTION_STATUS getCurrentCharacterStatus() {
		return currentCharacterActionStatus;
	}

	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append("* GameInstance [");
		builder.append(mode);
		builder.append(":");
		builder.append(id);
		builder.append("]\n");
		builder.append("Board:");
		builder.append(board.toString(false));
		builder.append("* Characters:");
		for (int i = 0; i < characters.size(); i++) {
			ICharacter c = characters.get(i);
			builder.append("\n#" + i + " ");
			builder.append(c.getRoomIn().getRoomPosition() + " ");
			builder.append(c);
			if (!c.isReminderTokenUsable()) {
				builder.append("\n    ReminderToken: ");
				builder.append(c.getReminderToken().getRoomIn()
						.getRoomPosition());
			}
		}

		return builder.toString();
	}

	@Override
	public void gameOver(GAME_WINNER winner) {
		onGameOver(winner);
	}

	@Override
	public int getPrisonersCount() {
		// TODO Auto-generated method stub
		return 0;
	}
}